package checker;

import java.util.ArrayList;

import util.Visitor;
import util.AST.AST;
import util.AST.Program;
import util.AST.Command.Command;
import util.AST.Command.FunctionBody;
import util.AST.Command.FunctionDeclaration;
import util.AST.Command.VariableDeclaration;
import util.AST.Expression.BinaryExpression;
import util.AST.Expression.BooleanUnaryExpression;
import util.AST.Expression.Expression;
import util.AST.Expression.IdentifierUnaryExpression;
import util.AST.Expression.NumberUnaryExpression;
import util.AST.Expression.UnaryExpression;
import util.AST.RHS.CallStatementRHS;
import util.AST.RHS.ExpressionRHS;
import util.AST.Statement.AssignStatement;
import util.AST.Statement.BreakStatement;
import util.AST.Statement.CallStatement;
import util.AST.Statement.ContinueStatement;
import util.AST.Statement.IfElseStatement;
import util.AST.Statement.PrintlnStatement;
import util.AST.Statement.ReturnStatement;
import util.AST.Statement.Statement;
import util.AST.Statement.WhileStatement;
import util.AST.Terminal.BooleanValue;
import util.AST.Terminal.Identifier;
import util.AST.Terminal.NumberValue;
import util.AST.Terminal.Operator;
import util.AST.Terminal.Type;
import util.symbolsTable.IdentificationTable;

public class Checker implements Visitor {
	private IdentificationTable table;
	private FunctionDeclaration functionCurrent;
	
	public Checker(){
		this.table = new IdentificationTable();
	}
	
	/**
	 * Init check.*/
	public AST check(Program prog) throws SemanticException{
		AST resp = (AST)prog.visit(this, null);
		if(table.containsKey("main")){
			Object o = table.retrieve("main");
			if(o instanceof FunctionDeclaration){
				FunctionDeclaration fd = (FunctionDeclaration)o;
				ArrayList<VariableDeclaration> parameters = fd.getParameters();
				if(!fd.getReturnType().getSpelling().equals("void") || !parameters.isEmpty())
					throw new SemanticException("Error in defined function - main() - return type void and empty parameters!");
				else
					return resp;
			}
		}
		throw new SemanticException("Undefined function - void main(){...}!");
	}
	
	public Object visitProgram(Program prog, Object arg) throws SemanticException{
		for(Command c : prog.getCommands() ){
			c.visit(this, prog);
		}
		return prog;
	}

	/**
	 * arg is FunctionDeclaration*/
	public Object visitFunctionBody(FunctionBody funB, Object arg) throws SemanticException {
		for(VariableDeclaration vd : funB.getVariables())
			vd.visit(this, arg);
		for(Statement s: funB.getStatements())
			s.visit(this, arg);
		return funB;
	}

	public Object visitFunctionDeclaration(FunctionDeclaration com, Object arg) throws SemanticException {
		functionCurrent = com;
		String id = com.getFunctionName().getSpelling();
		table.enter(id, com);
		String tipo = (String)com.getReturnType().visit(this, arg);
		com.getFunctionName().visit(this, com);
		table.openScope();
		for(VariableDeclaration vd : com.getParameters())
			vd.visit(this, com);
		com.getFunctionBody().visit(this,com); //arg is FunctionDeclaration.
		table.closeScope();
		if(com.getListReturn().size()==0 && !tipo.equals("void")){
			throw new SemanticException("Return of function '"+ com.getFunctionName().getSpelling() +"' not found!");
		}
		return com;
	}

	public Object visitVariableDeclaration(VariableDeclaration com, Object arg) throws SemanticException {
		String id = com.getIdentifier().getSpelling();
		table.enter(id, com);
		String tipo = com.getType().getSpelling();
		if(!tipo.equals("void")){
			com.getIdentifier().visit(this, com);
			return com;
		}else
			throw new SemanticException("Invalid declaration! ' "+ tipo +" ' in identifier " + com.getIdentifier().getSpelling() + "!");	
	}

	/**
	 * arg is functionCurrent the scope.*/
	public Object visitBinaryExpression(BinaryExpression expr, Object arg) throws SemanticException {
		String tipo1 = (String)expr.getLeftExpression().visit(this, arg);
		String tipo2 = (String)expr.getRightExpression().visit(this, arg);
		if(tipo1.equals(tipo2)){
			String op = (String)expr.getOperator().visit(this, tipo1);
			expr.setTipo(op);
			return expr.getTipo();
		}else
			throw new SemanticException("Incompatible type: "+tipo1+" != "+tipo2+" in function - "+functionCurrent.getFunctionName().getSpelling());
	}

	public Object visitBooleanUnaryExpression(BooleanUnaryExpression expr,Object arg) throws SemanticException {
		String tipo = (String)expr.getBooleanValue().visit(this, arg);
		expr.setTipo(tipo);
		return expr.getTipo();
	}

	public Object visitIdentifierUnaryExpression(IdentifierUnaryExpression expr, Object arg) throws SemanticException {
		String tipo = (String)expr.getVariableName().visit(this, arg);
		expr.setTipo(tipo);
		return expr.getTipo();
	}

	public Object visitNumberUnaryExpression(NumberUnaryExpression expr,Object arg) throws SemanticException {
		String tipo = (String)expr.getNumberValue().visit(this, arg);
		expr.setTipo(tipo);
		return expr.getTipo();
	}

	public Object visitUnaryExpression(UnaryExpression unExp, Object arg) throws SemanticException {
		if(unExp instanceof NumberUnaryExpression){
			NumberUnaryExpression nue = (NumberUnaryExpression)unExp;
			unExp.setTipo((String)nue.visit(this, arg));
		}else if(unExp instanceof BooleanUnaryExpression){
			BooleanUnaryExpression bue =(BooleanUnaryExpression)unExp;
			unExp.setTipo((String)bue.visit(this, arg));
		}else{
			IdentifierUnaryExpression iue = (IdentifierUnaryExpression)unExp;
			unExp.setTipo((String)iue.visit(this, arg));
		}
		return unExp.getTipo();
	}

	public Object visitCallStatementRHS(CallStatementRHS rhs, Object arg) throws SemanticException {
		rhs.setTipo((String)rhs.getFunctionCall().visit(this, arg));
		return rhs.getTipo();
	}

	public Object visitExpressionRHS(ExpressionRHS expRHS, Object arg) throws SemanticException {
		Expression exp = expRHS.getExpression();
		if(exp instanceof UnaryExpression){
			UnaryExpression ue = (UnaryExpression) exp;
			expRHS.setTipo((String)ue.visit(this, arg));
			return expRHS.getTipo();
		}
		else{
			BinaryExpression be = (BinaryExpression) exp;
			expRHS.setTipo((String)be.visit(this, arg));
			return expRHS.getTipo();
		}
	}

	public Object visitAssignStatement(AssignStatement statement, Object arg) throws SemanticException {
		String nomeVariavel = statement.getVariableName().getSpelling();
		String tipoExp = (String)statement.getRightHandStatement().visit(this, null);
		if(table.containsKey(nomeVariavel)){
			AST a = table.retrieve(nomeVariavel);
			if(!(a instanceof VariableDeclaration))
				throw new SemanticException("Attribution to only one variable! - '"+ ((FunctionDeclaration)a).getFunctionName().getSpelling()+"' is function!");
			VariableDeclaration vd = (VariableDeclaration)a;
			statement.getVariableName().visit(this, arg);
			statement.getRightHandStatement().visit(this, arg);
			String tipoVariavel = vd.getType().getSpelling();
			if(tipoExp.equals(tipoVariavel)){
				return tipoVariavel;
			}else
				throw new SemanticException("Incompatible types - " + tipoVariavel + " and " + tipoExp+" in function '"+functionCurrent.getFunctionName().getSpelling()+"'!");
		}else throw new SemanticException("The variable '"+nomeVariavel+"' was not declared!");
	}

	public Object visitBreakStatement(BreakStatement statement, Object arg) throws SemanticException {
		if(!(arg instanceof WhileStatement)){
			throw new SemanticException("'Break' can only be used at while!");
		}
		return statement;
	}

	public Object visitCallStatement(CallStatement statement, Object arg) throws SemanticException {
		String id = statement.getFunctionName().getSpelling();
		if(table.containsKey(id)){
			FunctionDeclaration fd = (FunctionDeclaration)table.retrieve(id);
			if(fd.getParameters().size() != statement.getArguments().size())
				throw new SemanticException("Invalid list of arguments in Funcion '"+id+"'.");
			
			for(int i=0;i<fd.getParameters().size();i++){ //testa se todos os argumentos s�o do mesmo tipo dos parametros.
				String parameter_i = fd.getParameters().get(i).getIdentifier().getTipo();
				String argumenter_i = (String)(statement.getArguments().get(i).visit(this, arg));
				
				if(!parameter_i.equals(argumenter_i)){
					throw new SemanticException("Invalid argument '"+ statement.getArguments().get(i).getSpelling()+"' in function "+id);
				}	
			}
			statement.setTipo((String)statement.getFunctionName().visit(this, arg));
			return statement.getTipo();
		}else{
			throw new SemanticException("Undeclared Function '" + id+"'!");
		}
	}

	public Object visitContinueStatement(ContinueStatement statement, Object arg) throws SemanticException {
		if(!(arg instanceof WhileStatement)){
			throw new SemanticException("'Continue' can only be used at while!");
		}
		return statement;
	}

	public Object visitIfElseStatement(IfElseStatement statement, Object arg) throws SemanticException {
		String tipo = (String)statement.getCondition().visit(this, arg);
		if(tipo.equals("boolean")){
			for(Statement s:statement.getIfStatements()) //statements for if
				s.visit(this, arg);
			for(Statement s:statement.getElseStatements()) //statements for else
				s.visit(this, arg);
			return statement;
		}else{
			throw new SemanticException("Invalid condition type! '"+ tipo+"'");
		}
	}

	public Object visitPrintlnStatement(PrintlnStatement statement, Object arg) throws SemanticException {
		statement.getVariableName().visit(this, arg);
		return statement;
	}

	public Object visitReturnStatement(ReturnStatement statement, Object arg) throws SemanticException {
		Object o = statement.getReturnExpression();
		FunctionDeclaration fd = functionCurrent;
		String tipoEsperado = fd.getReturnType().getSpelling();
		String tipo;
		if(o != null){
			tipo = (String)statement.getReturnExpression().visit(this, arg);
			if(!tipo.equals(tipoEsperado))
				throw new SemanticException("Unexpected return type '"+ tipo+"'!");
		}else{
			if(!fd.getReturnType().getSpelling().equals("void"))
				throw new SemanticException("Invalid return in function "+fd.getFunctionName().getSpelling()+"!");
		}
		fd.getListReturn().add(statement);
		return statement;
	}

	public Object visitWhileStatement(WhileStatement statement, Object arg) throws SemanticException {
		String tipo = (String)statement.getCondition().visit(this, arg);
		if(tipo.equals("boolean")){
			for(Statement s:statement.getStatements()){ //statements for while
				s.visit(this, statement);
			}
			return statement;
		}else{
			throw new SemanticException("Invalid condition type! '"+ tipo+"'!");
		}
	}

	public Object visitBooleanValue(BooleanValue value, Object arg) throws SemanticException {
		value.setTipo("boolean");
		return value.getTipo();
	}

	public Object visitIdentifier(Identifier id, Object arg) throws SemanticException{
		Object teste = table.retrieve(id.getSpelling());
		if(teste != null){
			if(teste instanceof VariableDeclaration){
				VariableDeclaration vd = (VariableDeclaration)teste;
				id.setTipo(vd.getType().getSpelling());
				id.setNodeDeclaration(vd);
			}
			else{
				FunctionDeclaration fd = (FunctionDeclaration)teste;
				id.setTipo(fd.getReturnType().getSpelling());
				id.setNodeDeclaration(fd);
			}
			return id.getTipo();
		}else{
			throw new SemanticException("Identifier '"+id.getSpelling()+"' not found!");
		}
	}

	public Object visitNumberValue(NumberValue value, Object arg) {
		if(value.getSpelling().contains(".")){
			value.setTipo("double");
		}else
			value.setTipo("int");
		return value.getTipo();
	}

	/**
	 * arg is type the operands.*/
	public Object visitOperator(Operator operator, Object arg) throws SemanticException{
		String math = "+-/*"; 		//math - (int and double) return int or double
		String logic2 = "!==";		//logic - (int, double and boolean) return boolean
		
		String tipo = (String)arg;  //type the operands.
		
		if(tipo.equals("int") || tipo.equals("double")){
			if(math.contains(operator.getSpelling()))
				return tipo; 
			else
				return "boolean";
		}else
			if(logic2.contains(operator.getSpelling()))
				return "boolean";
		throw new SemanticException("This operator '"+operator.getSpelling()+"' is not applyed to the current operands!");
	}

	public Object visitType(Type type, Object arg) {
		return type.getSpelling();
	}
	
}
